/*
Copyright 2024 New Vector Ltd.
Copyright 2015 OpenMarket Ltd

SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details.
 */

#import "MXKRoomBubbleComponent.h"

#import "MXEvent+MatrixKit.h"
#import "MXKSwiftHeader.h"
#import <MatrixSDK/MatrixSDK.h>

@interface MXKRoomBubbleComponent ()

@property (nonatomic, readwrite) id<MXThreadProtocol> thread;

@end

@implementation MXKRoomBubbleComponent

- (instancetype)initWithEvent:(MXEvent*)event
                    roomState:(MXRoomState*)roomState
           andLatestRoomState:(MXRoomState*)latestRoomState
               eventFormatter:(MXKEventFormatter*)eventFormatter
                      session:(MXSession*)session;
{
    if (self = [super init])
    {
        // Build text component related to this event
        _eventFormatter = eventFormatter;
        MXKEventFormatterError error;

        NSAttributedString *eventString = [_eventFormatter attributedStringFromEvent:event
                                                                       withRoomState:roomState
                                                                  andLatestRoomState:latestRoomState
                                                                               error:&error];
        
        // Store the potential error
        event.mxkEventFormatterError = error;
        
        _textMessage = nil;
        _attributedTextMessage = eventString;
        
        // Set date time
        if (event.originServerTs != kMXUndefinedTimestamp)
        {
            _date = [NSDate dateWithTimeIntervalSince1970:(double)event.originServerTs/1000];
        }
        else
        {
            _date = nil;
        }
        
        // Keep ref on event (used to handle the read marker, or a potential event redaction).
        _event = event;

        _displayFix = MXKRoomBubbleComponentDisplayFixNone;
        
        NSString *format = event.content[@"format"];
        if ([format isKindOfClass:[NSString class]] && [format isEqualToString:kMXRoomMessageFormatHTML])
        {
            NSString *formattedBody = (NSString*)event.content[@"formatted_body"];
            if ([formattedBody isKindOfClass:[NSString class]] && [formattedBody containsString:@"<blockquote"])
            {
                _displayFix |= MXKRoomBubbleComponentDisplayFixHtmlBlockquote;
            }
        }
        
        _encryptionDecoration = [self encryptionDecorationForEvent:event roomState:(MXRoomState*)roomState session:session];
        
        [self updateLinkWithRoomState:roomState];

        if (event.unsignedData.relations.thread)
        {
            self.thread = [[MXThreadModel alloc] initWithRootEvent:event
                                                 notificationCount:0
                                                    highlightCount:0];
        }
        else
        {
            self.thread = [session.threadingService threadWithId:event.eventId];
        }
    }
    return self;
}

- (void)updateWithEvent:(MXEvent*)event
              roomState:(MXRoomState*)roomState
     andLatestRoomState:(MXRoomState*)latestRoomState
                session:(MXSession*)session
{
    // Report the new event
    _event = event;

    if (_event.isRedactedEvent)
    {
        // Do not use the live room state for redacted events as they occurred in the past
        // Note: as we don't have valid room state in this case, userId will be used as display name
        roomState = nil;
    }
    // Other calls to updateWithEvent are made to update the state of an event (ex: MXKEventStateSending to MXKEventStateDefault).
    // They occur in live so we can use the room up-to-date state without making huge errors

    _textMessage = nil;

    MXKEventFormatterError error;
    _attributedTextMessage = [_eventFormatter attributedStringFromEvent:event
                                                          withRoomState:roomState
                                                     andLatestRoomState:latestRoomState
                                                                  error:&error];
    
    _encryptionDecoration = [self encryptionDecorationForEvent:event roomState:roomState session:session];
    
    [self updateLinkWithRoomState:roomState];
}

- (NSString *)textMessage
{
    if (!_textMessage)
    {
        _textMessage = _attributedTextMessage.string;
    }
    return _textMessage;
}

- (void)updateLinkWithRoomState:(MXRoomState*)roomState
{
    // Ensure link detection has been enabled
    if (!MXKAppSettings.standardAppSettings.enableBubbleComponentLinkDetection)
    {
        return;
    }
    
    // Only detect links in unencrypted rooms, for un-redacted message events that are text, notice or emote.
    // Specifically check the room's encryption state rather than the event's as outgoing events are always unencrypted initially.
    if (roomState.isEncrypted || self.event.eventType != MXEventTypeRoomMessage || [self.event isRedactedEvent])
    {
        self.link = nil;    // Ensure there's no link for a redacted event
        return;
    }
    
    NSString *messageType = self.event.content[kMXMessageTypeKey];
    
    if (!messageType || !([messageType isEqualToString:kMXMessageTypeText] || [messageType isEqualToString:kMXMessageTypeNotice] || [messageType isEqualToString:kMXMessageTypeEmote]))
    {
        return;
    }
    
    // Detect links in the attributed string which gets updated when the message is edited.
    // Restrict detection to the unquoted string so links are only found in the sender's message.
    NSString *body = [self.attributedTextMessage mxk_unquotedString];
    NSURL *url = [body mxk_firstURLDetected];
    
    if (!url)
    {
        self.link = nil;
        return;
    }
    
    self.link = url;
}

- (EventEncryptionDecoration)encryptionDecorationForEvent:(MXEvent*)event roomState:(MXRoomState*)roomState session:(MXSession*)session
{
    // Warning badges are unnecessary in unencrypted rooms
    if (!roomState.isEncrypted)
    {
        return EventEncryptionDecorationNone;
    }
    
    // Not all events are encrypted (e.g. state/reactions/redactions) and we only have encrypted cell subclasses for messages and attachments.
    if (event.eventType != MXEventTypeRoomMessage && !event.isMediaAttachment)
    {
        return EventEncryptionDecorationNone;
    }
    
    // Always show a warning badge if there was a decryption error.
    if (event.decryptionError)
    {
        return EventEncryptionDecorationRed;
    }
    
    // Unencrypted message events should show a warning unless they're pending local echoes
    if (!event.isEncrypted)
    {
        if (event.isLocalEvent
            || event.contentHasBeenEdited)    // Local echo for an edit is clear but uses a true event id, the one of the edited event
        {
            return EventEncryptionDecorationNone;
        }
            
        return EventEncryptionDecorationRed;
    }
    
    // The encryption is in a good state.
    // Only show a warning badge if there are decryption trust issues.
    if (event.decryptionDecoration)
    {
        switch (event.decryptionDecoration.color)
        {
            case MXEventDecryptionDecorationColorNone:
                return EventEncryptionDecorationNone;
            case MXEventDecryptionDecorationColorGrey:
                return EventEncryptionDecorationGrey;
            case MXEventDecryptionDecorationColorRed:
                return EventEncryptionDecorationRed;
        }
    }
    else
    {
        return EventEncryptionDecorationNone;
    }
}

@end

